﻿//#define VISTA_64
using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Microsoft.Win32;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace RawInput
{
   public sealed class InputDevice : NativeWindow
   {
      #region const definitions

      private const int RIDEV_INPUTSINK = 0x00000100;
      private const int RIDEV_NOLEGACY = 0x00000030;
      private const int RID_INPUT = 0x10000003;

      private const int FAPPCOMMAND_MASK = 0xF000;
      private const int FAPPCOMMAND_MOUSE = 0x8000;
      private const int FAPPCOMMAND_OEM = 0x1000;

      private const int RIM_TYPEMOUSE = 0;
      private const int RIM_TYPEKEYBOARD = 1;
      private const int RIM_TYPEHID = 2;

      private const int RIDI_DEVICENAME = 0x20000007;

      private const int WM_KEYDOWN = 0x0100;
      private const int WM_SYSKEYDOWN = 0x0104;
      private const int WM_INPUT = 0x00FF;
      private const int VK_OEM_CLEAR = 0xFE;
      private const int VK_LAST_KEY = VK_OEM_CLEAR; // this is a made up value used as a sentinal

      #endregion const definitions

      #region structs & enums

      public enum DeviceType
      {
         Key,
         Mouse,
         OEM
      }

      #region Windows.h structure declarations
      [StructLayout(LayoutKind.Sequential)]
      internal struct RAWINPUTDEVICELIST
      {
         public IntPtr hDevice;
         [MarshalAs(UnmanagedType.U4)]
         public int dwType;
      }

#if VISTA_64
      [StructLayout(LayoutKind.Explicit)]
      internal struct RAWINPUT
      {
         [FieldOffset(0)]
         public RAWINPUTHEADER header;
         [FieldOffset(24)]
         public RAWMOUSE mouse;
         [FieldOffset(24)]
         public RAWKEYBOARD keyboard;
         [FieldOffset(24)]
         public RAWHID hid;
      }

      [StructLayout(LayoutKind.Sequential)]
      internal struct RAWINPUTHEADER
      {
         [MarshalAs(UnmanagedType.U4)]
         public int dwType;
         [MarshalAs(UnmanagedType.U4)]
         public int dwSize;
         public IntPtr hDevice;
         public IntPtr wParam;
      }
#else
      [StructLayout(LayoutKind.Explicit)]
      internal struct RAWINPUT
      {
         [FieldOffset(0)]
         public RAWINPUTHEADER header;
         [FieldOffset(16)]
         public RAWMOUSE mouse;
         [FieldOffset(16)]
         public RAWKEYBOARD keyboard;
         [FieldOffset(16)]
         public RAWHID hid;
      }

      [StructLayout(LayoutKind.Sequential)]
      internal struct RAWINPUTHEADER
      {
         [MarshalAs(UnmanagedType.U4)]
         public int dwType;
         [MarshalAs(UnmanagedType.U4)]
         public int dwSize;
         public IntPtr hDevice;
         [MarshalAs(UnmanagedType.U4)]
         public int wParam;
      }
#endif

      [StructLayout(LayoutKind.Sequential)]
      internal struct RAWHID
      {
         [MarshalAs(UnmanagedType.U4)]
         public int dwSizHid;
         [MarshalAs(UnmanagedType.U4)]
         public int dwCount;
      }

      [StructLayout(LayoutKind.Sequential)]
      internal struct BUTTONSSTR
      {
         [MarshalAs(UnmanagedType.U2)]
         public ushort usButtonFlags;
         [MarshalAs(UnmanagedType.U2)]
         public ushort usButtonData;
      }

      [StructLayout(LayoutKind.Explicit)]
      internal struct RAWMOUSE
      {
         [MarshalAs(UnmanagedType.U2)]
         [FieldOffset(0)]
         public ushort usFlags;
         [MarshalAs(UnmanagedType.U4)]
         [FieldOffset(4)]
         public uint ulButtons;
         [FieldOffset(4)]
         public BUTTONSSTR buttonsStr;
         [MarshalAs(UnmanagedType.U4)]
         [FieldOffset(8)]
         public uint ulRawButtons;
         [FieldOffset(12)]
         public int lLastX;
         [FieldOffset(16)]
         public int lLastY;
         [MarshalAs(UnmanagedType.U4)]
         [FieldOffset(20)]
         public uint ulExtraInformation;
      }

      [StructLayout(LayoutKind.Sequential)]
      internal struct RAWKEYBOARD
      {
         [MarshalAs(UnmanagedType.U2)]
         public ushort MakeCode;
         [MarshalAs(UnmanagedType.U2)]
         public ushort Flags;
         [MarshalAs(UnmanagedType.U2)]
         public ushort Reserved;
         [MarshalAs(UnmanagedType.U2)]
         public ushort VKey;
         [MarshalAs(UnmanagedType.U4)]
         public uint Message;
         [MarshalAs(UnmanagedType.U4)]
         public uint ExtraInformation;
      }

      [StructLayout(LayoutKind.Sequential)]
      internal struct RAWINPUTDEVICE
      {
         [MarshalAs(UnmanagedType.U2)]
         public ushort usUsagePage;
         [MarshalAs(UnmanagedType.U2)]
         public ushort usUsage;
         [MarshalAs(UnmanagedType.U4)]
         public int dwFlags;
         public IntPtr hwndTarget;
      }
      #endregion Windows.h structure declarations

      /// <summary>
      /// Class encapsulating the information about a
      /// keyboard event, including the device it
      /// originated with and what key was pressed
      /// </summary>
      public class DeviceInfo
      {
         public string deviceName;
         public string deviceType;
         public IntPtr deviceHandle;
         public string Name;
         public string source;
         public ushort key;
         public string vKey;
      }

      #endregion structs & enums

      #region DllImports

      [DllImport("User32.dll")]
      extern static uint GetRawInputDeviceList(IntPtr pRawInputDeviceList, ref uint uiNumDevices, uint cbSize);

      [DllImport("User32.dll")]
      extern static uint GetRawInputDeviceInfo(IntPtr hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize);

      [DllImport("User32.dll")]
      extern static bool RegisterRawInputDevices(RAWINPUTDEVICE[] pRawInputDevice, uint uiNumDevices, uint cbSize);

      [DllImport("User32.dll")]
      extern static uint GetRawInputData(IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader);

      #endregion DllImports

      #region Variables and event handling

      /// <summary>
      /// List of keyboard devices
      /// Key: the device handle
      /// Value: the device info class
      /// </summary>
      private Dictionary<IntPtr, DeviceInfo> m_dicDeviceList = new Dictionary<IntPtr, DeviceInfo>();
      private Dictionary<string, int> m_dicDeviceNameToOrdinal = new Dictionary<string, int>();


      //Event and delegate
      public delegate void DeviceEventHandler(object sender, int scanCode, int deviceId);
      public event DeviceEventHandler KeyPressed;

      #endregion Variables and event handling

      #region InputDevice( IntPtr hwnd )

      /// <summary>
      /// InputDevice constructor; registers the raw input devices
      /// for the calling window.
      /// </summary>
      /// <param name="hwnd">Handle of the window listening for key presses</param>
      public InputDevice()
      {
         CreateHandle(new CreateParams());
         RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];

         rid[0].usUsagePage = 0x01;
         rid[0].usUsage = 0x06;
         rid[0].dwFlags = RIDEV_INPUTSINK;
         rid[0].hwndTarget = Handle;

         if (!RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(rid[0])))
         {
            throw new ApplicationException("Failed to register raw input device(s).");
         }
         EnumerateDevices();
      }

      #endregion InputDevice( IntPtr hwnd )

      #region ReadReg( string item, ref bool isKeyboard )

      /// <summary>
      /// Reads the Registry to retrieve a friendly description
      /// of the device, and whether it is a keyboard.
      /// </summary>
      /// <param name="item">The device name to search for, as provided by GetRawInputDeviceInfo.</param>
      /// <param name="isKeyboard">Determines whether the device's class is "Keyboard". By reference.</param>
      /// <returns>The device description stored in the Registry entry's DeviceDesc value.</returns>
      private static string ReadReg(string item, ref bool isKeyboard)
      {
         // Example Device Identification string
         // @"\??\ACPI#PNP0303#3&13c0b0c5&0#{884b96c3-56ef-11d1-bc8c-00a0c91405dd}";

         // remove the \??\
         item = item.Substring(4);

         string[] split = item.Split('#');

         string id_01 = split[0];    // ACPI (Class code)
         string id_02 = split[1];    // PNP0303 (SubClass code)
         string id_03 = split[2];    // 3&13c0b0c5&0 (Protocol code)
         //The final part is the class GUID and is not needed here

         //Open the appropriate key as read-only so no permissions
         //are needed.
         RegistryKey OurKey = Registry.LocalMachine;

         string findme = string.Format(@"System\CurrentControlSet\Enum\{0}\{1}\{2}", id_01, id_02, id_03);

         OurKey = OurKey.OpenSubKey(findme, false);

         //Retrieve the desired information and set isKeyboard
         string deviceDesc = (string)OurKey.GetValue("DeviceDesc");
         string deviceClass = (string)OurKey.GetValue("Class");

         if (deviceClass.ToUpper().Equals("KEYBOARD"))
         {
            isKeyboard = true;
         }
         else
         {
            isKeyboard = false;
         }
         return deviceDesc;
      }

      #endregion ReadReg( string item, ref bool isKeyboard )

      #region int EnumerateDevices()

      /// <summary>
      /// Iterates through the list provided by GetRawInputDeviceList,
      /// counting keyboard devices and adding them to deviceList.
      /// </summary>
      /// <returns>The number of keyboard devices found.</returns>
      public int EnumerateDevices()
      {
         List<string> names = new List<string>();
         int NumberOfDevices = 0;
         uint deviceCount = 0;
         int dwSize = (Marshal.SizeOf(typeof(RAWINPUTDEVICELIST)));

         if (GetRawInputDeviceList(IntPtr.Zero, ref deviceCount, (uint)dwSize) == 0)
         {
            IntPtr pRawInputDeviceList = Marshal.AllocHGlobal((int)(dwSize * deviceCount));
            GetRawInputDeviceList(pRawInputDeviceList, ref deviceCount, (uint)dwSize);

            for (int i = 0; i < deviceCount; i++)
            {
               uint pcbSize = 0;

               RAWINPUTDEVICELIST rid = (RAWINPUTDEVICELIST)Marshal.PtrToStructure(
                                          new IntPtr((pRawInputDeviceList.ToInt32() + (dwSize * i))),
                                          typeof(RAWINPUTDEVICELIST));

               GetRawInputDeviceInfo(rid.hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref pcbSize);

               if (pcbSize > 0)
               {
                  IntPtr pData = Marshal.AllocHGlobal((int)pcbSize);
                  GetRawInputDeviceInfo(rid.hDevice, RIDI_DEVICENAME, pData, ref pcbSize);
                  string deviceName = Marshal.PtrToStringAnsi(pData);

                  //The list will include the "root" keyboard and mouse devices
                  //which appear to be the remote access devices used by Terminal
                  //Services or the Remote Desktop - we're not interested in these
                  //so the following code with drop into the next loop iteration
                  if (deviceName.ToUpper().Contains("ROOT"))
                  {
                     continue;
                  }

                  //If the device is identified as a keyboard or HID device,
                  //create a DeviceInfo object to store information about it
                  if (rid.dwType == RIM_TYPEKEYBOARD || rid.dwType == RIM_TYPEHID)
                  {
                     DeviceInfo dInfo = new DeviceInfo();

                     dInfo.deviceName = Marshal.PtrToStringAnsi(pData);
                     dInfo.deviceHandle = rid.hDevice;
                     dInfo.deviceType = GetDeviceType(rid.dwType);

                     //Check the Registry to see whether this is actually a 
                     //keyboard.
                     bool IsKeyboardDevice = false;

                     string DeviceDesc = ReadReg(deviceName, ref IsKeyboardDevice);
                     dInfo.Name = DeviceDesc;

                     //If it is a keyboard and it isn't already in the list,
                     //add it to the deviceList hashtable and increase the
                     //NumberOfDevices count
                     if (!m_dicDeviceList.ContainsKey(rid.hDevice) && IsKeyboardDevice)
                     {
                        NumberOfDevices++;
                        m_dicDeviceList.Add(rid.hDevice, dInfo);
                        names.Add(dInfo.Name);
                     }
                  }
                  Marshal.FreeHGlobal(pData);
               }
            }

            Marshal.FreeHGlobal(pRawInputDeviceList);
            foreach (string name in names.OrderBy(y => y))
            {
               m_dicDeviceNameToOrdinal.Add(name, m_dicDeviceNameToOrdinal.Count);
            }
            return NumberOfDevices;
         }
         else
         {
            throw new ApplicationException("Error!");
         }

      }

      #endregion EnumerateDevices()

      #region ProcessInputCommand( Message message )

      /// <summary>
      /// Processes WM_INPUT messages to retrieve information about any
      /// keyboard events that occur.
      /// </summary>
      /// <param name="message">The WM_INPUT message to process.</param>
      public void ProcessInputCommand(Message message)
      {
         uint dwSize = 0;

         // First call to GetRawInputData sets the value of dwSize
         // dwSize can then be used to allocate the appropriate amount of memore,
         // storing the pointer in "buffer".
         GetRawInputData(message.LParam,
                          RID_INPUT, IntPtr.Zero,
                          ref dwSize,
                          (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER)));

         IntPtr buffer = Marshal.AllocHGlobal((int)dwSize);
         try
         {
            // Check that buffer points to something, and if so,
            // call GetRawInputData again to fill the allocated memory
            // with information about the input
            if (buffer != IntPtr.Zero &&
                GetRawInputData(message.LParam,
                                 RID_INPUT,
                                 buffer,
                                 ref dwSize,
                                 (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))) == dwSize)
            {
               // Store the message information in "raw", then check
               // that the input comes from a keyboard device before
               // processing it to raise an appropriate KeyPressed event.

               RAWINPUT raw = (RAWINPUT)Marshal.PtrToStructure(buffer, typeof(RAWINPUT));

               if (raw.header.dwType == RIM_TYPEKEYBOARD)
               {
                  if (raw.keyboard.Message == WM_KEYDOWN || raw.keyboard.Message == WM_SYSKEYDOWN)
                  {

                     // Retrieve information about the keystroke
                     ushort key = raw.keyboard.VKey;

                     // On most keyboards, "extended" keys such as the arrow or page 
                     // keys return two codes - the key's own code, and an "extended key" flag, which
                     // translates to 255. This flag isn't useful to us, so it can be
                     // disregarded.
                     if (key > VK_LAST_KEY)
                     {
                        return;
                     }

                     Keys myKey;  // Keys is defined in System.Windows.Forms 
                     int vkey;

                     vkey = raw.keyboard.VKey;

                     myKey = (Keys)Enum.Parse(typeof(Keys), Enum.GetName(typeof(Keys), vkey));

                     // Retrieve information about the device
                     DeviceInfo dInfo = null;


                     if (m_dicDeviceList.ContainsKey(raw.header.hDevice))
                     {
                        dInfo = (DeviceInfo)m_dicDeviceList[raw.header.hDevice];

                        dInfo.vKey = myKey.ToString();
                        dInfo.key = key;
                     }

                     int iScanCode = raw.keyboard.MakeCode;
                     if ((raw.keyboard.Flags & 2) != 0)
                        iScanCode += 256;

                     if (iScanCode == 256)
                     {
                        // Scancode is 0, this is probably a multimedia key generated by some input device
                        // use the vkey instead
                        iScanCode = 512 + raw.keyboard.VKey;
                     }

                     if (KeyPressed != null && dInfo != null)
                     {
                        int deviceId = -1;
                        if (m_dicDeviceNameToOrdinal.ContainsKey(dInfo.Name))
                           deviceId = m_dicDeviceNameToOrdinal[dInfo.Name];
                        KeyPressed(this, iScanCode, deviceId);
                     }
                     else
                     {
                        KeyPressed(this, iScanCode, -1);
                     }
                  }
               }
            }
         }
         finally
         {
            Marshal.FreeHGlobal(buffer);
         }
      }

      #endregion ProcessInputCommand( Message message )

      #region DeviceType GetDevice( int param )

      /// <summary>
      /// Determines what type of device triggered a WM_INPUT message.
      /// (Used in the ProcessInputCommand).
      /// </summary>
      /// <param name="param">The LParam from a WM_INPUT message.</param>
      /// <returns>A DeviceType enum value.</returns>
      private static DeviceType GetDevice(int param)
      {
         DeviceType deviceType;

         switch ((((ushort)(param >> 16)) & FAPPCOMMAND_MASK))
         {
            case FAPPCOMMAND_OEM:
               deviceType = DeviceType.OEM;
               break;
            case FAPPCOMMAND_MOUSE:
               deviceType = DeviceType.Mouse;
               break;
            default:
               deviceType = DeviceType.Key;
               break;
         }
         return deviceType;
      }

      #endregion DeviceType GetDevice( int param )

      #region ProcessMessage(Message message)

      /// <summary>
      /// Filters Windows messages for WM_INPUT messages and calls
      /// ProcessInputCommand if necessary.
      /// </summary>
      /// <param name="message">The Windows message.</param>
      public void ProcessMessage(Message message)
      {
         switch (message.Msg)
         {
            case WM_INPUT:
               {
                  ProcessInputCommand(message);
               }
               break;
         }
      }

      #endregion ProcessMessage( Message message )

      #region GetDeviceType( int device )

      /// <summary>
      /// Converts a RAWINPUTDEVICELIST dwType value to a string
      /// describing the device type.
      /// </summary>
      /// <param name="device">A dwType value (RIM_TYPEMOUSE, 
      /// RIM_TYPEKEYBOARD or RIM_TYPEHID).</param>
      /// <returns>A string representation of the input value.</returns>
      private static string GetDeviceType(int device)
      {
         string deviceType;
         switch (device)
         {
            case RIM_TYPEMOUSE: deviceType = "MOUSE"; break;
            case RIM_TYPEKEYBOARD: deviceType = "KEYBOARD"; break;
            case RIM_TYPEHID: deviceType = "HID"; break;
            default: deviceType = "UNKNOWN"; break;
         }
         return deviceType;
      }

      #endregion GetDeviceType( int device )

      // The WndProc is overridden to allow InputDevice to intercept
      // messages to the window and thus catch WM_INPUT messages
      protected override void WndProc(ref Message message)
      {
         ProcessMessage(message);
         base.WndProc(ref message);
      }

   }
}
